﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Content;
using Tesla.Util;

namespace Tesla.Core {

    /// <summary>
    /// Buffer class that acts like a Stream. It has relative Get/Set
    /// operations to copy byte data to and from the buffer regardless
    /// of buffer contents.
    /// </summary>
    public abstract class DataBuffer : ISavable {
        private int _position;
        private int _length;

        /// <summary>
        /// Get the buffer as an array of bytes. This will always
        /// return a new byte array snapshot.
        /// </summary>
        public abstract byte[] ByteDataCopy {
            get;
        }

        /// <summary>
        /// Get the entire size of the buffer contents in bytes.
        /// </summary>
        public abstract int SizeInBytes {
            get;
        }

        /// <summary>
        /// Get the size of a single element in the buffer in bytes.
        /// </summary>
        public abstract int ElementSizeInBytes {
            get;
        }

        /// <summary>
        /// Gets the element type contained in the buffer.
        /// </summary>
        public abstract Type ElementType {
            get;
        }

        /// <summary>
        /// Get if this buffer'fullyQualifiedName position can be incremented to the next index.
        /// </summary>
        public bool HasNext {
            get {
                return _position < _length;
            }
        }

        /// <summary>
        /// Get or set the current buffer position. This index must
        /// be between 0 and capacity - 1.
        /// </summary>
        public int Position {
            get {
                return _position;
            }
            set {
                if(value < 0) {
                    _position = 0;
                } else if(value >= _length) {
                    _position = _length - 1;
                } else {
                    _position = value;
                }
            }
        }

        /// <summary>
        /// Gets or protected sets the number of elements this buffer can contain.
        /// </summary>
        public int Length {
            get {
                return _length;
            }
            protected set {
                _length = value;
            }
        }

        /// <summary>
        /// Relative set operation that copies byte data into the buffer.
        /// </summary>
        /// <param name="data">Byte data to copy to the buffer</param>
        public void Set(byte[] data) {
            Set(data, 0, data.Length);
        }

        /// <summary>
        /// Relative set operation that copies byte data into the buffer.
        /// </summary>
        /// <param name="data">Byte data to copy to the buffer</param>
        /// <param name="count">Number of bytes to copy</param>
        public void Set(byte[] data, int count) {
            Set(data, 0, count);
        }

        /// <summary>
        /// Relative set operation that copies by te data into the buffer.
        /// </summary>
        /// <param name="data">Byte data to copy to the buffer</param>
        /// <param name="byteOffset">Zero-based byte index to read from the byte store</param>
        /// <param name="byteCount">Number of bytes to copy</param>
        public abstract void Set(byte[] data, int byteOffset, int byteCount);

        /// <summary>
        /// Relative get operation that copies byte store from the buffer.
        /// </summary>
        /// <param name="store">Byte array to store copied byte data</param>
        public void Get(byte[] store) {
            Get(store, 0, store.Length);
        }

        /// <summary>
        /// Relative get operation that copies byte store from the buffer.
        /// </summary>
        /// <param name="store">Byte array to store copied byte data</param>
        /// <param name="count">Number of bytes to copy</param>
        public void Get(byte[] store, int count) {
            Get(store, 0, count);
        }

        /// <summary>
        /// Relative get operation that copies byte store from the buffer.
        /// </summary>
        /// <param name="store">Byte array to store copied byte data</param>
        /// <param name="byteOffset">Zero-based byte index to start copying into the byte store</param>
        /// <param name="byteCount">Number of bytes to copy</param>
        public abstract void Get(byte[] store, int byteOffset, int byteCount);

        /// <summary>
        /// Checks the array bounds.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <returns>True if within range</returns>
        protected bool CheckBounds(int index) {
            if((index >= _length) || (index < 0)) {
                return false;
            }
            return true;
        }

        /// <summary>
        /// Checks the index.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <returns>Index, if valid</returns>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the index is out of range</exception>
        protected int CheckIndex(int index) {
            if(CheckBounds(index)) {
                return index;
            } else {
                throw new ArgumentOutOfRangeException("Index is out of range.");
            }
        }

        /// <summary>
        /// Checks if the array can be accessed starting from its index and
        /// the number of elements to process.
        /// </summary>
        /// <param name="length">The array length.</param>
        /// <param name="index">The starting index.</param>
        /// <param name="count">The element count.</param>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if any of the input values are out of range.</exception>
        protected void CheckIndex(int length, int index, int count) {
            if(count < 0) {
                throw new ArgumentOutOfRangeException("Count must be greater than or equal to zero.");
            }
            if(index < 0) {
                throw new ArgumentOutOfRangeException("Offset is out of range.");
            }
            if(index + count > length) {
                throw new ArgumentOutOfRangeException("Count and offset must be less than the array length.");
            }
        }

        /// <summary>
        /// Gets the next valid index.
        /// </summary>
        /// <returns>Next index</returns>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if trying to access a position beyond the valid length.</exception>
        protected int GetNextIndex() {
            if(_position >= _length) {
                throw new ArgumentOutOfRangeException("Cannot advance buffer beyond capacity.");
            }
            return _position++;
        }

        /// <summary>
        /// Serializes the object and writes it to the output.
        /// </summary>
        /// <param name="output">Savable Output</param>
        public abstract void Write(ISavableWriter output);

        /// <summary>
        /// Deserializes the object and populates it from the input.
        /// </summary>
        /// <param name="input">Savable input</param>
        public abstract void Read(ISavableReader input);
    }

    /// <summary>
    /// Generic buffer class. Has additional relative and absolute Get/Set operations.
    /// </summary>
    /// <typeparam name="T">Generic type that is bounded to a value type</typeparam>
    public sealed class DataBuffer<T> : DataBuffer where T : struct {
        private T[] _buffer;

        /// <summary>
        /// Get the underlying managed buffer that contains this buffer'fullyQualifiedName data. 
        /// </summary>
        public T[] Buffer {
            get {
                return _buffer;
            }
        }

        /// <summary>
        /// Get the buffer as an array of bytes. This will always
        /// return a new byte array snapshot.
        /// </summary>
        public override byte[] ByteDataCopy {
            get {
                int totalSize = Length * ElementSizeInBytes;
                byte[] byteData = new byte[totalSize];
                MemoryHelper.Copy(_buffer, byteData, totalSize);
                return byteData;
            }
        }

        /// <summary>
        /// Get the size of a single element in the buffer in bytes.
        /// </summary>
        public override int ElementSizeInBytes {
            get {
                return MemoryHelper.SizeOf<T>();
            }
        }

        /// <summary>
        /// Gets the element type contained in the buffer.
        /// </summary>
        public override Type ElementType {
            get {
                return typeof(T);
            }
        }

        /// <summary>
        /// Get the entire size of the buffer contents in bytes.
        /// </summary>
        public override int SizeInBytes {
            get {
                return MemoryHelper.SizeOf<T>() * _buffer.Length;
            }
        }

        /// <summary>
        /// For ISavable, do not use.
        /// </summary>
        private DataBuffer() { }

        /// <summary>
        /// Instantiates a new DataBuffer with initial capacity.
        /// </summary>
        /// <param name="capacity">Number of elements the buffer can hold, with index range of [0, Length - 1]</param>
        /// <exception cref="System.ArgumentException">Thrown if the capacity is not greater than zero.</exception>
        public DataBuffer(int capacity) {
            _buffer = new T[capacity];
            if(capacity <= 0) {
                throw new ArgumentException("DataBuffer capacity must be greater than zero!");
            }
            Length = capacity;
            Position = 0;
        }

        /// <summary>
        /// Instantiates a new DataBuffer initialized with the input data, which will be used as the backing store.
        /// </summary>
        /// <param name="data">Array of values that will act as the backing store</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the data is null or length is zero.</exception>
        public DataBuffer(params T[] data) {
            if(data == null || data.Length == 0) {
                throw new ArgumentNullException("Data array cannot be null!");
            }

            _buffer = data;
            Length = data.Length;
            Position = 0;
        }

        /// <summary>
        /// Relative set operation. Increments the position to the next value space and
        /// stores the value.
        /// </summary>
        /// <param name="value">Value to store</param>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity.</exception>
        public void Set(T value) {
            _buffer[GetNextIndex()] = value;
        }

        /// <summary>
        /// Absolute set operation. Stores the value at the specified index, if its
        /// within range.
        /// </summary>
        /// <param name="value">Value to store</param>
        /// <param name="index">Index to store the value at</param>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the index is not within [0, Length - 1] range.</exception>
        public void Set(T value, int index) {
            _buffer[CheckIndex(index)] = value;
        }

        /// <summary>
        /// Relative bulk set operation. Stores all the array's values, incrementing the position
        /// of the buffer as it goes. The buffer must have sufficient space and bounds checks are enforced.
        /// </summary>
        /// <param name="data">Array data to copy from</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the array is null or length is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity</exception>
        public void Set(T[] data) {
            if(data == null || data.Length == 0) {
                throw new ArgumentNullException("Input array is null");
            }
            Set(data, 0, data.Length);
        }

        /// <summary>
        /// Relative bulk set operation. Stores the specified number of the values in the input array into
        /// the buffer, incrementing the position of the buffer as it goes. The buffer must have sufficient 
        /// space and bounds checks are enforced.
        /// </summary>
        /// <param name="data">Array data to copy from</param>
        /// <param name="count">Number of elements to copy</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the array is null or length is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity</exception>
        public void Set(T[] data, int count) {
            Set(data, 0, count);
        }

        /// <summary>
        /// Relative bulk set operation. Stores the specified number of the values in the input array into
        /// the buffer, incrementing the position of the buffer as it goes. The buffer must have sufficient 
        /// space and bounds checks are enforced.
        /// </summary>
        /// <param name="data">Array data to copy from</param>
        /// <param name="startIndex">Zero-based index of the array at which to start reading from</param>
        /// <param name="count">Number of elements to copy</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the array is null or length is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity</exception>
        public void Set(T[] data, int startIndex, int count) {
            if(data == null || data.Length == 0) {
                throw new ArgumentNullException("Input array is null");
            }

            CheckIndex(data.Length, startIndex, count);

            if(Position + count > Length) {
                throw new ArgumentOutOfRangeException("Number of elements to copy exceeds buffer capacity.");
            }

            int size = ElementSizeInBytes;
            MemoryHelper.Copy(data, startIndex * size, _buffer, Position * size, count * size);
            Position += count;
        }

        /// <summary>
        /// Relative set operation that copies by te data into the buffer.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="byteOffset">Zero-based byte index to read from the byte store</param>
        /// <param name="byteCount">Number of bytes to write</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the array is null or length is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity</exception>
        public override void Set(byte[] data, int byteOffset, int byteCount) {
            if(data == null || data.Length == 0) {
                throw new ArgumentNullException("Input array is null");
            }

            CheckIndex(data.Length, byteOffset, byteCount);

            int size = ElementSizeInBytes;

            if(Position + (byteCount / size) > Length) {
                throw new ArgumentException("Buffer is out of space, ensure the capacity is adequate to hold the input data!");
            }

            MemoryHelper.Copy(data, byteOffset, _buffer, Position * size, byteCount);
            Position += byteCount / size;
        }

        /// <summary>
        /// Relative get operation. Returns the next element and increments position by one.
        /// </summary>
        /// <returns>Next element in the buffer</returns>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity</exception>
        public T Get() {
            return _buffer[GetNextIndex()];
        }

        /// <summary>
        /// Absolute get operation. Returns the element at the specified index, provided
        /// it is within range.
        /// </summary>
        /// <param name="index">Index of the value in the buffer</param>
        /// <returns>The value</returns>
        /// <exception cref="System.ArgumentOutOfRangeException">Throws an exception if the index is outside the range of [0, Length - 1]</exception>
        public T Get(int index) {
            return _buffer[CheckIndex(index)];
        }

        /// <summary>
        /// Relative bulk get operation. Copies the data from the buffer at the current position
        /// to the end of the array store.
        /// </summary>
        /// <param name="store">Array to store the copied data from the buffer</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the array is null or length is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity</exception>
        public void Get(T[] store) {
            Get(store, 0, store.Length);
        }

        /// <summary>
        /// Relative bulk get operation. Copies data from the buffer at the current position
        /// to position + count to the array.
        /// </summary>
        /// <param name="store">Array to store the copied data from the buffer</param>
        /// <param name="count">Number of elements to copy</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the array is null or length is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity</exception>
        public void Get(T[] store, int count) {
            Get(store, 0, count);
        }

        /// <summary>
        /// Relative bulk get operation. Copies data from the buffer at the current position
        /// to position + count to the array starting at the startIndex.
        /// </summary>
        /// <param name="store">Array to store the copied data from the buffer</param>
        /// <param name="startIndex">Zero-based index of the array at which to start writing values</param>
        /// <param name="count">Number of elements to copy</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the array is null or length is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity</exception>
        public void Get(T[] store, int startIndex, int count) {
            if(store == null) {
                throw new ArgumentNullException("Input array is null");
            }

            CheckIndex(store.Length, startIndex, count);

            if(Position + count > Length) {
                throw new ArgumentException("Buffer is out of space, ensure the capacity is adequate to hold the input data!");
            }

            int size = ElementSizeInBytes;
            MemoryHelper.Copy(_buffer, Position * size, store, startIndex * size, count * size);
            Position += count;
        }

        /// <summary>
        /// Relative get operation that copies byte store from the buffer.
        /// </summary>
        /// <param name="store">Byte array to store copied byte data</param>
        /// <param name="byteOffset">Zero-based byte index to start copying into the byte store</param>
        /// <param name="byteCount">Number of bytes to read</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the array is null or length is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the operation will exceed the buffer's available capacity</exception>
        public override void Get(byte[] store, int byteOffset, int byteCount) {
            if(store == null) {
                throw new ArgumentNullException("Input array is null");
            }

            CheckIndex(store.Length, byteOffset, byteCount);

            int size = ElementSizeInBytes;

            if(Position + (byteCount / size) > Length) {
                throw new ArgumentException("Buffer is out of space, ensure the capacity is adequate to hold the input data!");
            }

            MemoryHelper.Copy(_buffer, Position * size, store, byteOffset, byteCount);
            Position += byteCount / size;
        }

        /// <summary>
        /// Serializes the object and writes it to the output.
        /// </summary>
        /// <param name="output">Savable Output</param>
        public override void Write(ISavableWriter output) {
            output.Write("Data", ByteDataCopy);
        }

        /// <summary>
        /// Deserializes the object and populates it from the input.
        /// </summary>
        /// <param name="input">Savable input</param>
        public override void Read(ISavableReader input) {
            byte[] byteData = input.ReadByteArray();
            int capacity = byteData.Length / MemoryHelper.SizeOf<T>();
            _buffer = new T[capacity];
            Length = capacity;
            Position = 0;

            Set(byteData);
            Position = 0;
        }
    }
}
